// =============================================================================
//  STATS - Statistical Analysis Tools, v.0.01
//  Copyright (C) 2012  Marco Vettigli
// -----------------------------------------------------------------------------
//
// This program  is free software:  you can  redistribute it and/or  modify it
// under the terms of the  GNU General Public License as published by the Free
// Software Foundation,  either version 3 of the License,  or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY;  without even  the  implied  warranty  of  MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE.  See the  GNU General Public License  for
// more details.
//
// You  should  have received a copy of the  GNU General Public License  along
// with this program.  If not, see <http://www.gnu.org/licenses/>.
//
// == module ===================================================================
//
// SDOUBLEAXIS.CPP
// The class represents a axis of continuous values with variable properties
// to be used in the construction of complex graphs.
//
// Author: Marco Vettigli
//
// TODO: write SDoubleAxis module documentation
//
// == implementation ===========================================================
//
// TODO: write SDoubleAxis implementation documentation
//
// =============================================================================

// == include directives =======================================================

#include <QInputDialog>
#include <QPainter>
#include <QMenu>
#include <QDebug>
#include <cmath>
#include "sdoubleaxis.h"
#include "gui/dialogdoubleaxisproperties.h"

// == constructors and destructors =============================================

StatS::SDoubleAxis::SDoubleAxis(QWidget *parent) :
    QWidget(parent)
{
    // Initialize properties
    _size = 400;
    _min = 0;
    _max = 10;
    _increment = 3;
    _minTicks = 3;
    // inizialize axis state
    _axisOrient = Horizontal;
    _title = "Untitled";
    _titleFont = QFont("Helvetica", 10, QFont::Normal);
    _reverseState = false;
    _minorTicksVisible = true;
    _majorTicksVisible = true;
    _minorGridVisible = true;
    _majorGridVisible = true;
    // initialize labels state
    _labelOrientation = Vertical;
    _labelsFont = QFont("Helvetica", 9, QFont::Normal);
    _labelsVisible = true;
    _labelsPrecision = 4;
    _labelsFormat = 'g';
    // initialize handle state
    _pressedPoint = QPoint(0, 0);
    _keyState = false;
    setMouseTracking(true);
    // set signals-slots
    setContextMenuPolicy(Qt::CustomContextMenu);
    connect (this, SIGNAL(customContextMenuRequested(QPoint)),
             this, SLOT(on_contextMenu(QPoint)));    
}

StatS::SDoubleAxis::~SDoubleAxis()
{
}

// == getters ==================================================================

int StatS::SDoubleAxis::size() const
{
    return _size;
}

double StatS::SDoubleAxis::min() const
{
    return _min;
}

double StatS::SDoubleAxis::max() const
{
    return _max;
}

double StatS::SDoubleAxis::range() const
{
    return _max - _min;
}

int StatS::SDoubleAxis::increment() const
{
    return _increment;
}

int StatS::SDoubleAxis::minorTicks() const
{
    return _minTicks;
}

QString StatS::SDoubleAxis::title() const
{
    return _title;
}

QFont StatS::SDoubleAxis::titleFont() const
{
    return _titleFont;
}

StatS::SDoubleAxis::Orientation StatS::SDoubleAxis::axisOrientation() const
{
    return _axisOrient;
}

StatS::SDoubleAxis::Orientation StatS::SDoubleAxis::labelOrientation() const
{
    return _labelOrientation;
}

QFont StatS::SDoubleAxis::labelFont() const
{
    return _labelsFont;
}

int StatS::SDoubleAxis::countMajorTicks() const
{
    double ticks = (_size - getMajorTickOffset()) / getMajorTickSize();
    return trunc(ticks);
}

int StatS::SDoubleAxis::countMinorTicks() const
{
    double ticks = (_size - getMinorTickOffset()) / getMinorTickSize();
    return trunc(ticks);
}

int StatS::SDoubleAxis::getZeroOffset() const
{
    QFontMetrics metrics(_labelsFont);
    double zeroOffset = 0;
    // calculate the zero ofset respect to frame border
    switch (_labelOrientation) {
    case Horizontal:
        zeroOffset = 0.5 * metrics.width(
                    QString("%1").arg(min(), 0, _labelsFormat,
                                      _labelsPrecision));
        break;
    case Vertical:
        zeroOffset = 0.5 * metrics.height();
        break;
    }
    return round(zeroOffset);
}

QSize StatS::SDoubleAxis::getFrameSize()
{
    int lenght, height;
    calculateFrameSize(lenght, height);
    switch (_axisOrient) {
    case Horizontal: return QSize(lenght, height);
    case Vertical: return QSize(height + 1, lenght + 1);
    }
    return QSize();
}

double StatS::SDoubleAxis::getMajorTickSize() const
{
    return _increment * _size / range();
}

double StatS::SDoubleAxis::getMajorTickOffset() const
{
    // get minimum multiple of increment..
    double minValue = trunc(_min / _increment) * _increment;
    // ..and take previous one based on its sign
    if (minValue < _min) minValue += _increment;
    // return the offset
    return (minValue - _min) / range() * _size;
}

QList<int> StatS::SDoubleAxis::getMajorTicksPosition() const
{
    QList<int> list;
    int x;
    for (int i = 0; i <= countMajorTicks(); i++) {
        x = round(getMajorTickOffset() + i * getMajorTickSize());
        if (_reverseState) list.append(_size - x);
        else list.append(x);
    }
    return list;
}

double StatS::SDoubleAxis::getMinorTickSize() const
{
    return getMajorTickSize() / _minTicks;
}

double StatS::SDoubleAxis::getMinorTickOffset() const
{
    double increment = _increment / _minTicks;
    // get minimum multiple of increment..
    double minValue = trunc(_min / increment) * increment;
    // ..and take previous one based on its sign
    if (minValue < _min) minValue += increment;
    // return the offset
    return std::abs(minValue - _min) / range() * _size;
}

QList<int> StatS::SDoubleAxis::getMinorTicksPosition() const
{
    QList<int> list;
    int x;
    for (int i = 0; i <= countMinorTicks(); i++) {
        x = round(getMinorTickOffset() + i * getMinorTickSize());
        if (_reverseState) list.append(_size - x);
        else list.append(x);
    }
    return list;
}

int StatS::SDoubleAxis::getValuePosition(double value, bool* in)
{
    double pos = (value - _min) / range() * _size;
    // switch position based on reverse state and axis orientation
    if (_reverseState) pos = _size - pos;
    // set in state true or false depending on axis size
    if (in != 0) {
        if (pos <= 0 || pos >= _size) *in = false;
        else *in = true;
    }
    // return position
    return round(pos);
}

int StatS::SDoubleAxis::getValueRange(double size, bool* in)
{
    // set in based on axis range
    if (in != 0) {
        if (size < 0 || size > range()) *in = false;
        else *in = true;
    }
    // return range
    return round(size / range() * _size);
}

// == setters ==================================================================

bool StatS::SDoubleAxis::setSize(int size)
{
    if (size < 0) return false;
    _size = size;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setMinimum(double min)
{
    // check which value is greater than the other..
    if (min > _max) {
        // .. and switch them
        int temp = _max;
        _max = min;
        min = temp;
    }
    // define minimum properties
    _min = min;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setMaximum(double max)
{
    // check which value is greater than the other..
    if (max < _min) {
        // .. and switch them
        int temp = _min;
        _min = max;
        max = temp;
    }
    // define maximum properties
    _max = max;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setRange(double min,
                                  double max)
{    
    // check which value is greater than the other..
    if (min > max) {
        // .. and switch them
        int temp = min;
        min = max;
        max = temp;
    }
    // define minimum and maximum properties
    _min = min;
    _max = max;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setRange(StatS::SDoubles* array)
{
    // check if array reference is valid
    if (!array) return false;
    if (!array->size()) return false;
    // set axis increment
    double increment = array->range() / 4;
    if (increment == 0) increment = std::abs(array->min() * 0.1);
    // set axis properties
    _min = array->min() - increment;
    _max = array->max() + increment;
    _increment = increment;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setIncrement(double increment)
{
    // check if argument is positive
    if (increment <= 0) return false;
    _increment = increment;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setMinorTicks(int num)
{
    if (num < 0) return false;
    _minTicks = num;
    return true;
}

bool StatS::SDoubleAxis::setPrecision(int precision)
{
    if (precision < -1 || precision == _labelsPrecision) return false;
    _labelsPrecision = precision;
    return true;
}

bool StatS::SDoubleAxis::setAxisOrientation(Orientation orientation)
{
    if (orientation == _axisOrient) return false;    
    _axisOrient = orientation;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setLabelOrientation(Orientation orientation)
{
    _labelOrientation = orientation;
    return true;
}

bool StatS::SDoubleAxis::setTitleFont(const QFont font)
{
    _titleFont = font;
    return true;
}

bool StatS::SDoubleAxis::setLabelFont(const QFont font)
{
    _labelsFont = font;
    return true;
}

bool StatS::SDoubleAxis::setTitle(const QString title)
{
    // check if title is empty or equal to current title
    if (title.isEmpty() || title == _title) return false;
    _title = title;
    emit(propertiesChanged());
    return true;
}

bool StatS::SDoubleAxis::setLabelsVisible(bool state)
{
    // check if current state is different from new one
    if (state == _labelsVisible) return true;
    // change label visibility state
    _labelsVisible = state;
    return true;
}

bool StatS::SDoubleAxis::setMajorTicksVisible(bool state)
{
    if (state == _majorTicksVisible) return false;
    _majorTicksVisible = state;
    return true;
}

bool StatS::SDoubleAxis::setMajorGridVisible(bool state)
{
    if (state == _majorGridVisible) return false;
    _majorGridVisible = state;
    return true;
}

bool StatS::SDoubleAxis::setMinorTicksVisible(bool state)
{
    if (state == _minorTicksVisible) return false;
    _minorTicksVisible = state;
    return true;
}

bool StatS::SDoubleAxis::setMinorGridVisible(bool state)
{
    if (state == _minorGridVisible) return false;
    _minorGridVisible = state;
    return true;
}

// == inspectors ===============================================================

bool StatS::SDoubleAxis::isLabelsVisible() const
{
    return _labelsVisible;
}

bool StatS::SDoubleAxis::isMajorTicksVisible() const
{
    return _majorTicksVisible;
}

bool StatS::SDoubleAxis::isMajorGridVisible() const
{
    return _majorGridVisible;
}

bool StatS::SDoubleAxis::isMinorTicksVisible() const
{
    return _minorTicksVisible;
}

bool StatS::SDoubleAxis::isMinorGridVisible() const
{
    return _minorGridVisible;
}

// == accessor function definitions ============================================

void StatS::SDoubleAxis::paintEvent(QPaintEvent* /*e*/)
{
    // initialize QPainter to perform painting
    // and set the font to _labelFont
    QPainter p(this);
    p.setFont(_labelsFont);
    // only for convenience, to get size of labels
    QFontMetrics metrics(_labelsFont);
    // initialize local variables
    int lenght, height;
    // calculate lenght and height of the frame
    calculateFrameSize(lenght, height);
    // rotate coordinate system and reset min/max
    // depending on axis orientation
    switch (_axisOrient) {
    case Horizontal:
        break;
    case Vertical:
        p.translate(height, 0);
        p.rotate(90);
        break;
    }
    // calculate the zero ofset respect to frame border
    int xOffset = getZeroOffset();
    QList<int> majorTicks = getMajorTicksPosition();
    QList<int> minorTicks = getMinorTicksPosition();    
    // draw x-axis
    p.drawLine(xOffset, 0, xOffset + _size, 0);
    // draw major ticks
    double x;
    if (_majorTicksVisible) {
        for (int i = 0; i < majorTicks.size(); i++) {                        
            if (_axisOrient == Vertical) x = xOffset + _size - majorTicks[i];
            else x = xOffset + majorTicks[i];
            p.drawLine(x, 0, x, TICKSIZE);
        }
    }
    // draw tick label
    if (_labelsVisible) {
        double mTickValue = trunc(_min / _increment) * _increment;
        if (mTickValue < _min) mTickValue += _increment;
        for (int i = 0; i < majorTicks.size(); i++) {
            double value = mTickValue + i * _increment;            
            if (abs(value/ _increment) < 1E-10) value = 0;
            if (_axisOrient == Vertical) x = xOffset + _size - majorTicks[i];
            else x = xOffset + majorTicks[i];

            QString label = QString("%1").arg(value, 0, _labelsFormat,
                                              _labelsPrecision);
            int xLabel, yLabel;
            switch (_labelOrientation) {
            case Horizontal:
                xLabel = x - metrics.width(label) / 2;
                yLabel = metrics.height() + TICKSIZE;
                p.drawText(xLabel, yLabel, label);
                break;
            case Vertical:
                p.rotate(-90);
                xLabel = -metrics.width(label) - TICKSIZE;
                yLabel = x + metrics.height()/4;
                p.drawText(xLabel, yLabel, label);
                p.rotate(90);
                break;
            }
        }
    }    
    // draw secondary ticks            
    if (_minorTicksVisible) {
        for (int i = 0; i < minorTicks.size(); i++) {            
            if (_axisOrient == Vertical) x = xOffset + _size - minorTicks[i];
            else x = xOffset + minorTicks[i];
            p.drawLine(x, 0, x, TICKSIZE / 2);
        }
    }
    // draw axis title    
    p.setFont(_titleFont);
    int xTitle, yTitle;
    switch (_axisOrient) {
    case Horizontal:
        xTitle = (lenght - QFontMetrics(_titleFont).width(_title)) / 2;
        yTitle = height - 0.5 * QFontMetrics(_titleFont).height();
        p.drawText(xTitle, yTitle, _title);
        break;
    case Vertical:
        p.rotate(180);
        xTitle = - (lenght + QFontMetrics(_titleFont).width(_title)) / 2;
        yTitle = - height + QFontMetrics(_titleFont).height();
        p.drawText(xTitle, yTitle, _title);
        p.rotate(-180);
        break;
    }
    // p.drawRect(0,0,lenght, height); // for debug only
    // set window size    
    p.setWindow(0,0, lenght, height);

}

// == static variables =========================================================

// == static funtion definitions ===============================================

// == private function definitions =============================================

void StatS::SDoubleAxis::calculateFrameSize(int &lenght,
                                            int &height)
{
    // only for convenience, to get size of labels
    QFontMetrics metrics(_labelsFont);
    // declare and assign variables
    int labelHeight = 0;
    lenght = _size; height = 0;
    // ..browse all labels to get the maximum size
    if (_labelsVisible) {
        QList<int> majorTicks = getMajorTicksPosition();
        double mTickValue = trunc(_min / _increment) * _increment;
        if (mTickValue < _min) mTickValue += _increment;
        for (int i = 0; i < majorTicks.size(); i++) {
            double value = mTickValue + i * _increment;
            if (abs(value/ _increment) < 1E-10) value = 0;
            QString label = QString("%1").arg(value, 0, _labelsFormat,
                                              _labelsPrecision);
            switch (_labelOrientation) {
            case Horizontal: labelHeight = metrics.height() + TICKSIZE; break;
            case Vertical: labelHeight = metrics.width(label) + TICKSIZE; break;
            }
            // set maximum height in height
            if (labelHeight > height) height = labelHeight;
            // if first or last label, add lateral overflow
            if (i == 0 || i == majorTicks.size() - 1)
                switch (_labelOrientation) {
                case Horizontal: lenght += metrics.width(label) / 2; break;
                case Vertical: lenght += metrics.height() / 2; break;
                }
        }
    }
    // add space for title
    height += 1.5 * QFontMetrics(_titleFont).height();    
    return;
}

void StatS::SDoubleAxis::on_contextMenu(QPoint pos)
{
    // create context menu and all submenus
   QMenu *contextMenu = new QMenu(this);
   // populate context menu
   contextMenu->addAction("Size", this, SLOT(on_sizeSetting()));
   contextMenu->addAction("Title", this, SLOT(on_titleSetting()));
   contextMenu->addAction("Reverse axis", this, SLOT(on_axisReversing()));
   contextMenu->addSeparator();
   contextMenu->addAction("Properties", this, SLOT(on_propertiesSetting()));

   // cast context menu at mouse point
   contextMenu->exec(mapToGlobal(pos));
   delete contextMenu;
}

void StatS::SDoubleAxis::on_propertiesSetting()
{
    DialogDoubleAxisProperties* dialog = new DialogDoubleAxisProperties(this);
    // set dialog properties
    dialog->setMaximum(_max);
    dialog->setMinimum(_min);
    dialog->setIncrement(_increment);
    dialog->setMinorTicks(_minTicks);
    dialog->setLabelsOrientation(_labelOrientation);
    dialog->setLabelsVisible(_labelsVisible);
    dialog->setMajorTicksVisible(_majorTicksVisible);
    dialog->setMajorGridVisible(_majorGridVisible);
    dialog->setMinorTicksVisible(_minorTicksVisible);
    dialog->setMinorGridVisible(_minorGridVisible);
    dialog->setLabelsFont(_labelsFont);
    dialog->setLabelsFormat(_labelsFormat);
    dialog->setLabelsPrecision(_labelsPrecision);
    // check if user confirmed changing
    if (dialog->exec() ==QDialog::Accepted) {
        // set new axis properties
        _max = dialog->max();
        _min = dialog->min();
        _increment = dialog->increment();
        _minTicks = dialog->minorTicks();
        _labelOrientation = dialog->labelsOrientation();
        _labelsVisible = dialog->isLabelsVisible();
        _majorTicksVisible = dialog->isMajorTicksVisible();
        _majorGridVisible = dialog->isMajorGridVisible();
        _minorTicksVisible = dialog->isMinorTicksVisible();
        _minorGridVisible = dialog->isMinorGridVisible();
        _labelsFont = dialog->labelsFont();
        _labelsFormat = dialog->labelsFormat();
        _labelsPrecision = dialog->labelsPrecision();
        //...
        emit(propertiesChanged());
    }
    // dispose objects and return
    delete dialog;
    return;
}

void StatS::SDoubleAxis::on_sizeSetting()
{
    // get new size from user
    bool dialogResult = false;
    int size = QInputDialog::getInt(
                this, "Axis size", "Enter axis size:",
                _size, 0, 2147483647, 1, &dialogResult);
    // check if user confirmed changing
    if (!dialogResult) return;
    setSize(size);
    return;
}

void StatS::SDoubleAxis::on_titleSetting()
{
    // get new title from user
    bool dialogResult = false;
    QString title = QInputDialog::getText(
                this, "Axis title", "Enter axis title:",
                QLineEdit::Normal, _title, &dialogResult);
    // check if user confirmed changing
    if (!dialogResult) return;
    setTitle(title);
    return;
}

void StatS::SDoubleAxis::on_axisReversing()
{
    _reverseState = !_reverseState;
    emit(propertiesChanged());
    return;
}

void StatS::SDoubleAxis::mouseMoveEvent(QMouseEvent *e)
{
    if (isInRightSizeHandle(e->pos())) {
        switch (_axisOrient) {
        case Horizontal: setCursor(Qt::SizeHorCursor); break;
        case Vertical: setCursor(Qt::SizeVerCursor); break;
        }
    } else setCursor(Qt::ArrowCursor);
}

void StatS::SDoubleAxis::mousePressEvent(QMouseEvent *e)
{
    if (!_keyState) {
        if (isInRightSizeHandle(e->pos())) {
            _pressedPoint = e->pos();
            _keyState = true;
        }
    }
    return;
}

void StatS::SDoubleAxis::mouseReleaseEvent(QMouseEvent *e)
{
    if (_keyState) {
        // right handle
        if (isInRightSizeHandle(_pressedPoint)) {
            switch (_axisOrient) {
            case Horizontal:
                setSize(size() + e->pos().x() - _pressedPoint.x());
                break;
            case Vertical: setCursor(Qt::SizeVerCursor);
                setSize(size() + e->pos().y() - _pressedPoint.y());
                break;
            }
        }
        _pressedPoint = QPoint(0,0);
        _keyState = false;
        emit(propertiesChanged());
    }
    return;
}

bool StatS::SDoubleAxis::isInRightSizeHandle(const QPoint point)
{
    QSize frame = getFrameSize();
    switch (_axisOrient) {
    case Horizontal:
        return QRect(frame.width() - 10, 0,
                     frame.width(), frame.height())
                .contains(point);
    case Vertical:
        return QRect(0, frame.height() - 10,
                     frame.width(), frame.height())
                .contains(point);
    }
    return false;
}
